package com.zoltu.GoogleAuthenticator;

import android.accounts.AbstractAccountAuthenticator;
import android.accounts.Account;
import android.accounts.AccountAuthenticatorResponse;
import android.accounts.AccountManager;
import android.accounts.NetworkErrorException;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.util.Log;

public class Authenticator extends AbstractAccountAuthenticator
{
	public static final int ERROR_CODE_BAD_CREDENTIALS = 0x01000001;
	public static final int ERROR_CODE_ACCOUNT_NOT_FOUND = 0x01000002;
	
	public static final String GOOGLE_ACCOUNT_TYPE = "com.google";
	public static final String GOOGLE_AUTH_TOKEN_TYPE = "GOOGLE";
	
	public static final String KEY_AUTH_TOKEN_TYPE = "authTokenType";
	public static final String KEY_REQUIRED_FEATURES = "requiredFeatures";
	public static final String KEY_LOGIN_OPTIONS = "loginOptions";
	public static final String KEY_ACCOUNT = "account";
	
	/**
	 * Authenticator should only be instantiated by AuthenticatorService so this context will be for that, not the app
	 * actually making the request.
	 **/
	Context mContext;
	
	public Authenticator(Context pContext)
	{
		super(pContext);
		
		mContext = pContext;
	}
	
	@Override public Bundle addAccount(AccountAuthenticatorResponse pResponse, String pAccountType, String pAuthTokenType, String[] pRequiredFeatures, Bundle pLoginOptions) throws NetworkErrorException
	{
		/* Validate the input. */
		try
		{
			ValidateAccountType(pAccountType);
			ValidateAuthTokenType(pAuthTokenType);
			ValidateRequiredFeatures(pRequiredFeatures);
		}
		catch (AuthenticatorException lException)
		{
			Log.w(getClass().getName(), "Validation on addAccount input failed.");
			lException.printStackTrace();
			return lException.GetFailureBundle();
		}
		
		/*
		 * Create and return an intent that will result in an activity that will prompt the user for credentials and add
		 * the account to the local credential store (database, preferences, etc.).
		 */
		Intent lIntent = new Intent(mContext, AddAccountActivity.class);
		lIntent.putExtra(AccountManager.KEY_ACCOUNT_MANAGER_RESPONSE, pResponse);
		lIntent.putExtra(AccountManager.KEY_ACCOUNT_TYPE, pAccountType);
		lIntent.putExtra(KEY_AUTH_TOKEN_TYPE, pAuthTokenType);
		lIntent.putExtra(KEY_REQUIRED_FEATURES, pRequiredFeatures);
		lIntent.putExtra(KEY_LOGIN_OPTIONS, pLoginOptions);
		AddActivityFlags(lIntent);
		
		Bundle lBundle = new Bundle();
		lBundle.putParcelable(AccountManager.KEY_INTENT, lIntent);
		
		return lBundle;
	}
	
	@Override public Bundle confirmCredentials(AccountAuthenticatorResponse pResponse, Account pAccount, Bundle pLoginOptions)
	{
		/* Validate the input. */
		try
		{
			ValidateAccountType(pAccount.type);
		}
		catch (AuthenticatorException lException)
		{
			return lException.GetFailureBundle();
		}
		
		/*
		 * Create and return an intent that will result in an activity that will prompt the user for credentials (though
		 * the account name is baked into the prompt) and then authenticate them and return true on success and false if
		 * they were unable to authenticate.
		 */
		Intent lIntent = new Intent(mContext, ConfirmCredentialsActivity.class);
		lIntent.putExtra(AccountManager.KEY_ACCOUNT_MANAGER_RESPONSE, pResponse);
		lIntent.putExtra(KEY_ACCOUNT, pAccount);
		lIntent.putExtra(KEY_LOGIN_OPTIONS, pLoginOptions);
		AddActivityFlags(lIntent);
		
		Bundle lBundle = new Bundle();
		lBundle.putParcelable(AccountManager.KEY_INTENT, lIntent);
		
		return lBundle;
	}
	
	@Override public Bundle editProperties(AccountAuthenticatorResponse pResponse, String pAccountType)
	{
		return null;
	}
	
	@Override public Bundle getAuthToken(AccountAuthenticatorResponse pResponse, Account pAccount, String pAuthTokenType, Bundle pLoginOptions) throws NetworkErrorException
	{
		/*
		 * TODO: Create a class that deals with all the inner workings of authentication. The class will be used here to
		 * authenticate the account and also by any authenticator Activities, which will be running in a different
		 * thread / process / application space (that of the application wishing to authenticate).
		 * 
		 * TODO: Find out if the authenticator Activities actually do run in the calling application's application space
		 * or if they are run in this application's space.
		 * 
		 * CONSIDER: Can we have AuthenticatorService do the authentication and have the authenticator Activities bind
		 * to it? This would allow us to easily consolidate the actual authentication code as well as keep all of the
		 * authentication code running in our application space instead of in the calling one's space.
		 * 
		 * CONSIDER: Can we spawn a new service that does the authentication? This would allow us to write it in a
		 * blocking fashion and the authenticator Activity or this class can spawn it to do the authentication work.
		 */
		return null;
	}
	
	@Override public String getAuthTokenLabel(String pAuthTokenType)
	{
		return null;
	}
	
	@Override public Bundle hasFeatures(AccountAuthenticatorResponse pResponse, Account pAccount, String[] pFeatures) throws NetworkErrorException
	{
		/* TODO: Find out what features the real com.google authenticator supports and support the same set. */

		Bundle lBundle = new Bundle();
		/* For now we always return false (support no features). */
		lBundle.putBoolean(AccountManager.KEY_BOOLEAN_RESULT, false);
		
		return lBundle;
	}
	
	@Override public Bundle updateCredentials(AccountAuthenticatorResponse pResponse, Account pAccount, String pAuthTokenType, Bundle pLoginOptions)
	{
		/* Validate the input. */
		try
		{
			ValidateAccountType(pAccount.type);
			ValidateAuthTokenType(pAuthTokenType);
		}
		catch (AuthenticatorException lException)
		{
			return lException.GetFailureBundle();
		}
		
		/*
		 * Create and return an intent that will result in an activity that will prompt the user for credentials and add
		 * the account to the local credential store (database, preferences, etc.).
		 */
		Intent lIntent = new Intent(mContext, UpdateCredentialsActivity.class);
		lIntent.putExtra(AccountManager.KEY_ACCOUNT_MANAGER_RESPONSE, pResponse);
		lIntent.putExtra(KEY_ACCOUNT, pAccount);
		lIntent.putExtra(KEY_AUTH_TOKEN_TYPE, pAuthTokenType);
		lIntent.putExtra(KEY_LOGIN_OPTIONS, pLoginOptions);
		AddActivityFlags(lIntent);
		
		Bundle lBundle = new Bundle();
		lBundle.putParcelable(AccountManager.KEY_INTENT, lIntent);
		
		return lBundle;
	}
	
	/**
	 * Helper function that will add a common set of flags to an intent whose intention is to launch a prompt to the
	 * user. These flags will cause the spawned activity to be in it's own task, disappear when it is navigated away
	 * from, not show up in the tasks history, and be flagged as coming from a background task.
	 * 
	 * @param pIntent
	 *            The intent to add the flags to.
	 **/
	private void AddActivityFlags(Intent pIntent)
	{
		pIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
		pIntent.addFlags(Intent.FLAG_ACTIVITY_MULTIPLE_TASK);
		pIntent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY);
		pIntent.addFlags(Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS);
		pIntent.addFlags(Intent.FLAG_FROM_BACKGROUND);
	}
	
	/**
	 * Helper function that will check to see if pAccountType is one we handle or not.
	 * 
	 * @param pAccountType
	 *            The account type to test.
	 * @throws UnSupportedAccountTypeException
	 *             is thrown if the provided account type is not one that is handled by this Authenticator. Included is
	 *             a failure bundle that can be returned by most of the functions in this class as a failure case.
	 */
	private void ValidateAccountType(String pAccountType) throws UnSupportedAccountTypeException
	{
		if (!pAccountType.equals(GOOGLE_ACCOUNT_TYPE))
		{
			throw new UnSupportedAccountTypeException();
		}
	}
	
	/**
	 * Helper function that will check to see if pAuthTokenType is one we handle or not.
	 * 
	 * @param pAuthTokenType
	 *            The auth token type to test.
	 * @throws UnsupportedAuthTokenTypeException
	 *             is thrown if the provided auth token type is not one that is handled by this Authenticator. Included
	 *             is a failure bundle that can be returned by most of the functions in this class as a failure case.
	 **/
	private void ValidateAuthTokenType(String pAuthTokenType) throws UnsupportedAuthTokenTypeException
	{
		if (!pAuthTokenType.equals(GOOGLE_AUTH_TOKEN_TYPE))
		{
			throw new UnsupportedAuthTokenTypeException();
		}
	}
	
	/**
	 * Helper function that will check to see if pFeatures are all provided.
	 * 
	 * @param pRequiredFeatures
	 *            The features to test to test.
	 * @throws UnsupportedFeaturesException
	 *             is thrown if the provided features are not all handled by this Authenticator. Included is a failure
	 *             bundle that can be returned by most of the functions in this class as a failure case.
	 **/
	private void ValidateRequiredFeatures(String[] pFeatures) throws UnsupportedFeaturesException
	{
		// TODO: Figure out what features are supported by com.google and throw an exception for any others.
	}
	
	/**
	 * Abstract exception for fail cases. Deals with building the failure bundle which can be returned from most
	 * AbstractAccountAuthenticator functions.
	 */
	private abstract class AuthenticatorException extends Exception
	{
		private static final long serialVersionUID = 1L;
		
		private Bundle mFailureBundle;
		
		protected AuthenticatorException(int pErrorCode, int pErrorMessageStringResourceID)
		{
			mFailureBundle = new Bundle();
			mFailureBundle.putInt(AccountManager.KEY_ERROR_CODE, pErrorCode);
			mFailureBundle.putString(AccountManager.KEY_ERROR_MESSAGE, mContext.getResources().getString(pErrorMessageStringResourceID));
		}
		
		public Bundle GetFailureBundle()
		{
			return mFailureBundle;
		}
	}
	
	/**
	 * Exception to throw when a provided account type is not supported by this authenticator.
	 */
	private class UnSupportedAccountTypeException extends AuthenticatorException
	{
		private static final long serialVersionUID = 1L;
		
		public UnSupportedAccountTypeException()
		{
			super(AccountManager.ERROR_CODE_UNSUPPORTED_OPERATION, R.string.error_unsupported_account_type);
		}
	}
	
	/**
	 * Exception to throw when a provided auth token type is not supported by this authenticator.
	 */
	private class UnsupportedAuthTokenTypeException extends AuthenticatorException
	{
		private static final long serialVersionUID = 1L;
		
		public UnsupportedAuthTokenTypeException()
		{
			super(AccountManager.ERROR_CODE_UNSUPPORTED_OPERATION, R.string.error_unsupported_auth_token_type);
		}
	}
	
	/**
	 * Exception to throw when a provided feature set is not supported by this authenticator.
	 */
	private class UnsupportedFeaturesException extends AuthenticatorException
	{
		private static final long serialVersionUID = 1L;
		
		public UnsupportedFeaturesException()
		{
			super(AccountManager.ERROR_CODE_UNSUPPORTED_OPERATION, R.string.error_unsupported_features);
		}
	}
}
